(ns jumpluff.commands
  (:require [cljs.core.async :as async]
            [clojure.string :as s]
            [jumpluff.state :as state]
            [jumpluff.poi-message :as poi-message]
            [jumpluff.config :refer [load-config write-config]]
            [jumpluff.datasets :refer [load-dataset]]
            [jumpluff.poi :refer [poi-search]]
            [jumpluff.utils :refer [log
                                    dataset-names
                                    dataset-names-some-exact-match
                                    server-emoji-id]])
  (:require-macros [cljs.core.async.macros :as async-macros]))

(def discord (js/require "discord.js"))

(defn log-poi-coll [coll log-msg]
  (log log-msg (take poi-message/*page-size* coll))
  coll)

(defn channel-id [channel]
  (str (if (not (nil? channel.guild))
         (.-id channel.guild)
         channel.type)
       "-" (or (.-parentID channel) "")
       "#" (or (.-name channel) channel.type)))

(defn dataset-name-for-channel
  "Get the default dataset name for `channel`, when unset return nil or `else`"
  ([channel]
   (get @state/channel-dataset (channel-id channel)))
  ([channel else]
   (get @state/channel-dataset (channel-id channel) else)))

(defn add-dataset
  ""
  [input-string]
  (let [ret (async/chan)
        args (remove empty? (s/split (s/trim input-string) " "))
        [name source] (take 2 args)
        [index-name index-gps index-description index-ex index-nicknames]
        (map #(let [n (js/parseInt %1)]
                (if (int? n) n nil))
             (drop 2 args))]
    (cond
      (not (not-any? nil? [name source index-name index-gps]))
      (async/put! ret (str "Incorrect params."
                           "\nExpected [name] [source] [index-name] [index-gps] [index-description?] [index-ex?] [index-nicknames?]"))
      (not (re-find #"^[\w-]*.csv$" source))
      (async/put! ret "Source as a filename isn't a plain csv filename")
      (not (apply distinct?
                  (remove nil? [index-name index-gps
                                index-description index-ex index-nicknames])))
      (async/put! ret "Indexes are not distinct")
      :else (do (swap! state/dataset-descriptions assoc name
                       {:source source
                        :columns {:index-name index-name
                                  :index-gps index-gps
                                  :index-description index-description
                                  :index-ex index-ex
                                  :index-nicknames index-nicknames}})
                (async-macros/go
                  (if (async/<! (write-config))
                    (async/put! ret "error writing config")
                    (async/put! ret (str "Added and saved: " (get @state/dataset-descriptions name)))))))
    ret))

(defn cmd-add-dataset
  ""
  [_message input-string]
  (add-dataset input-string))

(def inline-delimiter
  (or (not-empty js/process.env.JUMPLUFF_INLINE_DELIMITER)
      ","))
(def inline-collecting-regex--regex
  ;; #"(?s)\,{2}\s{0,3}([^,]{2,128})\s{0,3}\,{2}(.*)$"
  (-> "(?s)\\X{2}\\s{0,3}([^X]{2,128})\\s{0,3}\\X{2}(.*)$"
     (s/replace "X" inline-delimiter)
     re-pattern))
(defn inline-collecting-regex
  "Lazily collect each substring contained between double `inline-delimiter` in INPUT-STRING"
  [input-string & collected]
  (lazy-seq
   (if (empty? input-string)
     (reverse (remove nil? collected))
     (let [[full match rest] (re-find inline-collecting-regex--regex input-string)]
       (apply inline-collecting-regex rest match collected)))))

(defn cmd-inline
  "Default command run on each message.
  First check if the message.channel has a dataset-name registered.
  Then use `inline-collecting-regex` to collect up to 5 inline queries for `poi-search`"
  [message argument]
  (when-let [dataset-name (dataset-name-for-channel message.channel)]
    (when-let [queries (not-empty (take poi-message/*page-size* (inline-collecting-regex message.content)))]
      (log "—found inline queries: " (s/join ", " queries))
      (-> (flatten (map #(poi-search dataset-name %1) queries))
         (log-poi-coll "cmd-inline results: ")
         (poi-message/poi-collection-and-src-msg-to-obj message)
         poi-message/poi-collection-to-message))))

(defn cmd-channel-dataset
  [message argument]
  (let [ret (async/chan)]
    (cond (empty? argument)
          (async/put! ret
                      (dataset-name-for-channel message.channel
                                                "Channel does not have a default dataset"))
          (dataset-names-some-exact-match argument)
          (do (swap! state/channel-dataset
                     assoc (channel-id message.channel)
                     argument)
              (async-macros/go
                (if (async/<! (write-config))
                  (async/put! ret "error writing config")
                  (async/put! ret "Set and saved."))))
          :else
          (async/put! ret (str "Invalid dataset name."
                               "\nValid options are: "
                               (s/join ", " (dataset-names)))))
    ret))

(defn cmd-poi-query
  [message argument]
  (or (some-> (-> (dataset-name-for-channel message.channel)
                 (poi-search argument))
              (log-poi-coll "cmd-poi-query results: ")
              (poi-message/poi-collection-and-src-msg-to-obj message)
              poi-message/poi-collection-to-message)
      "Channel does not have a default dataset to act on and one was not provided in the query."))

(defn load-config-then-datasets
  "Use `data-dir` and `config-file` to fill `state/config` and `state/csv-files`"
  ([]
   (load-config-then-datasets state/data-dir state/config-file))
  ([data-dir config-file]
   (async-macros/go
     (async/<! (load-config config-file))
     (doseq [pair @state/dataset-descriptions]
       (async/<! (load-dataset state/csv-files data-dir pair))))))
(defn cmd-reload
  [message argument]
  (let [ret (async/chan)]
    (async-macros/go
      (async/<! (load-config-then-datasets))
      (async/put! ret "Reloaded config and datasets"))
    ret))

(defn cmd-set-emoji
  "register a discord(possibly server local) emoji for the bot to use
  to type a custom emoji in discord, there is an autocomplete after typing a colon"
  [message argument]
  (let [ret (async/chan)]
    (let [[name emoji] (s/split argument #" " 2)]
      (cond (and (= name "ex")
                 (not-empty emoji))
            (do (swap! state/server-emoji
                       assoc (server-emoji-id message name)
                       emoji)
                (async-macros/go
                  (if (async/<! (write-config))
                    (async/put! ret "error writing config")
                    (async/put! ret "Set and saved."))))
            :else
            (async/put! ret "Invalid arguments given. Expected 'ex :ex:'")))
    ret))

(defn set-dataset-note
  "attatch a message to a dataset id"
  [input-string]
  (let [ret (async/chan)]
    (let [[dataset-name note] (s/split input-string #" " 2)]
      (cond (dataset-names-some-exact-match dataset-name)
            (do (when-let [ds (get @state/dataset-descriptions dataset-name)]
                  (swap! state/dataset-descriptions assoc dataset-name
                         (assoc ds :note note)))
                (async-macros/go
                  (if (async/<! (write-config))
                    (async/put! ret "error writing config")
                    (async/put! ret "Set and saved."))))
            :else (async/put! ret "Given dataset name does not already exist.")))
    ret))
(defn cmd-set-dataset-note
  "attatch a message to a dataset id"
  [_message argument]
  (set-dataset-note argument))
(defn cmd-dataset-note
  [message argument]
  (if-let [dataset-name (if (not-empty argument)
                          (dataset-names-some-exact-match argument)
                          (dataset-name-for-channel message.channel))]
    (or (some->> dataset-name
                 (get @state/dataset-descriptions)
                 :note)
        "No saved note for this dataset")
    "Invalid or no default or no given dataset name to lookup"))

(defn add-dataset-abbrv
  "Add an abbreviation to expand before poi-filter-by-name for a dataset-name"
  [input-string]
  (let [ret (async/chan)]
    (let [[dataset-name abbrv expansion] (map s/trim (s/split input-string #" " 3))]
      (cond (dataset-names-some-exact-match dataset-name)
            (do (when-let [ds (get @state/dataset-descriptions dataset-name)]
                  (let [abbrvs (get ds :abbrv {})]
                    (swap! state/dataset-descriptions assoc dataset-name
                           (assoc ds :abbrv
                                  (assoc abbrvs abbrv expansion)))))
                (async-macros/go
                  (if (async/<! (write-config))
                    (async/put! ret "error writing config")
                    (async/put! ret "Set and saved."))))
            :else (async/put! ret "Given dataset name does not already exist.")))
    ret))
(defn cmd-add-dataset-abbrv
  [_message argument]
  (add-dataset-abbrv argument))
(defn cmd-dataset-abbrv
  [message argument]
  (if-let [dataset-name (if (not-empty argument)
                          (dataset-names-some-exact-match argument)
                          (dataset-name-for-channel message.channel))]
    (str (or (some->> dataset-name
                      (get @state/dataset-descriptions)
                      :abbrv)
             "No saved note for this dataset"))
    "Invalid or no default or no given dataset name to lookup"))

(defn cmd-help
  [message argument]
  (let [base (doto (discord.RichEmbed.)
               (.setTitle (str "Jumpluff(@" (.-user.tag @state/client) ") - Point of Interest Bot"))
               (.setDescription (str "Query spreadsheets of gym names/locations/ex eligible status"
                                     " - [source code](https://notabug.org/panselia/jumpluff)\n"))
               (.setFooter (if (not-empty js/process.env.JUMPLUFF_HOSTED_BY)
                             (str "Instance hosted by " js/process.env.JUMPLUFF_HOSTED_BY)
                             ""))
               (.addField "Setup and usage"
                          (str "Raid channels can be assigned a default dataset with `channel-dataset`.\n"
                               "In those channels you can query the dataset inline by wrapping the query with `"
                               inline-delimiter inline-delimiter "`"))
               (.addField "User commands"
                          (str (s/join ", " (map #(str "`" % "`") state/poi-query-prefixes)) " - Perform a query\n"
                               "`@" (.-user.username @state/client) " help [verbose?]` - this usage message\n"
                               "`@" (.-user.username @state/client) " dataset-note [dataset-name?]` - Print the dataset's note\n"
                               "`@" (.-user.username @state/client) " dataset-abbrv [dataset-name?]` - View PoI name expansions for the dataset\n")))]
    (if (empty? argument)
      base
      (doto base
        (.addField "Server admin commands via mention"
                   (str "`add-dataset` - Add a dataset, refer to the README for arguments\n"
                        "`channel-dataset [dataset-name?]` - Set a default dataset for the channel, or show the current default\n"
                        "`set-emoji ex :ex:` - Register a possibly server local emoji to use to show on EX gyms\n"
                        "`set-dataset-note [dataset-name] [note]` - Attach a note to a dataset name\n"
                        "`add-dataset-abbrv [dataset-name] [abbrv] [expansion]` - Add an abbreviation that expands before poi-filter-by-name matching\n"
                        "`reload` - reload the config and datasets"))
        (.addField "Number of servers this instance is in"
                   (str (count (.array (.-guilds @state/client)))))
        (.addField "Bot uptime" (str (.-uptime @state/client) "ms"))
        (.addField "Loaded dataset names"
                   (s/join ", " (dataset-names)))
        (.addField "EX eligible server emoji"
                   (get @state/server-emoji
                        (server-emoji-id message "ex")
                        " (EX)"))))))

(def commands
  {"add-dataset" {:privilege :admin :action cmd-add-dataset}
   "channel-dataset" {:privilege :admin :action cmd-channel-dataset}
   "set-emoji" {:privilege :admin :action cmd-set-emoji}
   "reload" {:privilege :admin :action cmd-reload}
   "set-dataset-note" {:privilege :admin :action cmd-set-dataset-note}
   "dataset-note" {:privilege :open :action cmd-dataset-note}
   "add-dataset-abbrv" {:privilege :admin :action cmd-add-dataset-abbrv}
   "dataset-abbrv" {:privilege :open :action cmd-dataset-abbrv}
   "help" {:privilege :open :action cmd-help}
   "poi" {:privilege :open :action cmd-poi-query}
   :inline {:privilege :open :action cmd-inline}})

(def commands-via-mention (remove #{:inline} (keys commands))) ; cache it
